@using Humanizer
@using StardewModdingAPI.Toolkit.Utilities
@using StardewModdingAPI.Web.Framework
@using StardewModdingAPI.Web.Framework.LogParsing.Models
@using StardewModdingAPI.Web.ViewModels
@model StardewModdingAPI.Web.ViewModels.LogParserModel

@{
    ViewData["Title"] = "SMAPI log parser";

    // get log info
    ParsedLog? log = Model.ParsedLog;
    IDictionary<string, LogModInfo[]> contentPacks = Model.GetContentPacksByMod();
    ISet<int> screenIds = new HashSet<int>(log?.Messages.Select(p => p.ScreenId) ?? Array.Empty<int>());

    // detect suggested fixes
    LogModInfo[] outdatedMods = log?.Mods.Where(mod => mod.HasUpdate).ToArray() ?? [];
    bool isPyTkCompatibilityMode = log?.ApiVersionParsed?.IsBetween("3.15.0", "3.19.0") is true && log.Mods.Any(p => p is { IsCodeMod: true, Name: "PyTK" } && p.GetParsedVersion()?.IsOlderThan("1.24.0") is true);

    LogModInfo? errorHandler = log?.Mods.FirstOrDefault(p => p is { IsCodeMod: true, Name: "Error Handler" });
    bool errorHandlerNeeded = log?.ApiVersionParsed?.IsOlderThan("4.0.0-alpha") == true && log.OperatingSystem?.Contains("Android Unix", StringComparison.OrdinalIgnoreCase) != true;
    bool missingErrorHandler = errorHandlerNeeded && errorHandler is null;
    bool hasOlderErrorHandler = errorHandlerNeeded && errorHandler?.GetParsedVersion() is not null && log?.ApiVersionParsed is not null && log.ApiVersionParsed.IsNewerThan(errorHandler.GetParsedVersion());

    // get filters
    IDictionary<string, bool> defaultFilters = Enum
        .GetValues<LogLevel>()
        .ToDictionary(level => level.ToString().ToLower(), level => level != LogLevel.Trace);
    IDictionary<int, string> logLevels = Enum
        .GetValues<LogLevel>()
        .ToDictionary(level => (int)level, level => level.ToString().ToLower());
    IDictionary<int, string> logSections = Enum
        .GetValues<LogSection>()
        .ToDictionary(section => (int)section, section => section.ToString());

    // get form
    string curPageUrl = this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteId }, absoluteUrl: true)!;
}

@section Head {
    @if (Model.PasteId != null)
    {
        <meta name="robots" content="noindex" />
    }
    <link rel="stylesheet" href="@Url.ContentWithCacheBust("~/Content/css/file-upload.css")" />
    <link rel="stylesheet" href="@Url.ContentWithCacheBust("~/Content/css/log-parser.css")" />
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tabbyjs@12.0.3/dist/css/tabby-ui-vertical.min.css" />

    <script src="https://cdn.jsdelivr.net/npm/tabbyjs@12.0.3" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14" crossorigin="anonymous"></script>
    <script src="https://cdn.jsdelivr.net/npm/jquery@3.5.1" crossorigin="anonymous"></script>
    <script src="@Url.ContentWithCacheBust("~/Content/js/file-upload.js")"></script>
    <script src="@Url.ContentWithCacheBust("~/Content/js/log-parser.js")"></script>

    <script id="serializedData" type="application/json">
        @if (!Model.ShowRaw)
        {
            <text>
                {
                    "messages": @this.ForJson(log?.Messages),
                    "sections": @this.ForJson(logSections),
                    "logLevels": @this.ForJson(logLevels),
                    "modSlugs": @this.ForJson(log?.Mods.DistinctBy(p => p.Name).Select(p => new {p.Name, Slug = Model.GetSlug(p.Name)}).Where(p => p.Name != p.Slug).ToDictionary(p => p.Name, p => p.Slug)),
                    "screenIds": @this.ForJson(screenIds)
                }
            </text>
        }
        else
        {
            <text>
                {
                    "messages": [],
                    "sections": {},
                    "logLevels": {},
                    "modSlugs": {},
                    "screenIds": []
                }
            </text>
        }
    </script>

    <script>
        $(function() {
            smapi.logParser(
                {
                    logStarted: new Date(@this.ForJson(log?.Timestamp)),
                    dataElement: "script#serializedData",
                    showPopup: @this.ForJson(log == null),
                    showMods: @this.ForJson(log?.Mods.Where(p => p is { Loaded: true, IsContentPack: false }).Select(p => Model.GetSlug(p.Name)).Distinct().ToDictionary(slug => slug, _ => true)),
                    showSections: @this.ForJson(Enum.GetNames(typeof(LogSection)).ToDictionary(section => section, _ => false)),
                    showLevels: @this.ForJson(defaultFilters),
                    enableFilters: @this.ForJson(!Model.ShowRaw),
                    isSplitScreen: @this.ForJson(log?.IsSplitScreen ?? false)
                }
            );

            @if (log == null)
            {
                <text>
                    new Tabby("[data-tabs]");
                </text>
            }
        });
    </script>
}

@* quick navigation links *@
@section SidebarExtra {
    @if (log != null)
    {
        <nav id="quickNav">
            <h4>Scroll to...</h4>
            <ul>
                <li><a href="#content">Top</a></li>
                <li><a href="#filterHolder">Log start</a></li>
                <li><a href="#footer">Bottom</a></li>
            </ul>
        </nav>
    }
}

@* upload result banner *@
@if (Model.UploadError != null)
{
    <div class="banner error" v-pre>
        <strong>Oops, the server ran into trouble saving that file.</strong><br />
        <small v-pre>Error details: @Model.UploadError</small>
    </div>
}
else if (Model.ParseError != null)
{
    <div class="banner error" v-pre>
        <strong>Oops, couldn't parse that log. (Make sure you upload the log file, not the console text.)</strong><br />
        Share this URL when asking for help: <code>@curPageUrl</code><br />
        (Or <a href="@this.Url.PlainAction("Index", "LogParser", values: null)">upload a new log</a>.)<br />
        <br />
        <small v-pre>Error details: @Model.ParseError</small>
    </div>
}
else if (log?.IsValid == true)
{
    <div class="banner success" v-pre>
        <strong>Share this link to let someone else see the log:</strong> <code>@curPageUrl</code><br />
        (Or <a href="@this.Url.PlainAction("Index", "LogParser", values: null)">upload a new log</a>.)
    </div>
}

@* save warnings *@
@if (Model.UploadWarning != null || Model.NewExpiry.HasValue)
{
    @if (Model.UploadWarning != null)
    {
        <text>⚠️ @Model.UploadWarning<br /></text>
    }

    <div class="save-metadata" v-pre>
        @if (Model.NewExpiry.HasValue)
        {
            string? oldExpiryText = Model.OldExpiry.HasValue
                ? (DateTime.UtcNow - Model.OldExpiry.Value).Humanize()
                : null;
            string? newExpiryText = Model.NewExpiry.HasValue
                ? (DateTime.UtcNow - Model.NewExpiry.Value).Humanize()
                : null;

            <text>This log will expire in </text>
            @if (oldExpiryText != null && oldExpiryText != newExpiryText)
            {
                <text><s>@(oldExpiryText)</s> </text>
            }
            <text>@(newExpiryText) (<a href="@(this.Url.PlainAction("Index", "LogParser", new { id = this.Model.PasteId, renew = true }))">renew</a>).</text>
        }
    </div>
}

@* upload new log *@
@if (log == null)
{
    <h2>Where do I find my SMAPI log?</h2>
    <div id="os-instructions">
        <div>
            <ul data-tabs>
                @foreach (Platform platform in new[] { Platform.Android, Platform.Linux, Platform.Mac, Platform.Windows })
                {
                    @if (platform == Platform.Windows)
                    {
                        <li><a data-tabby-default href="#@(platform)-steamgog">@platform (Steam or GOG)</a></li>
                        <li><a href="#@(platform)-xbox">@platform (Xbox app)</a></li>
                    }
                    else
                    {
                        <li><a href="#@platform">@platform</a></li>
                    }
                }
            </ul>
        </div>
        <div>
            <div id="@Platform.Android">
                <ol>
                    <li>Open a file app (like My Files or MT Manager).</li>
                    <li>Find the <code>StardewValley</code> folder on your internal storage.</li>
                    <li>Open the <code>ErrorLogs</code> subfolder.</li>
                    <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
                </ol>
            </div>
            <div id="@Platform.Linux">
                <ol>
                    <li>Open the Files app.</li>
                    <li>Click the options menu (might be labeled <em>Go</em> or <code>⋮</code>).</li>
                    <li>Choose <em>Enter Location</em>.</li>
                    <li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li>
                    <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
                </ol>
            </div>
            <div id="@Platform.Mac">
                <ol>
                    <li>Open the Finder app.</li>
                    <li>Click <em>Go</em> at the top, then <em>Go to Folder</em>.</li>
                    <li>Enter this exact text: <pre>~/.config/StardewValley/ErrorLogs</pre></li>
                    <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
                </ol>
            </div>
            <div id="@(Platform.Windows)-steamgog">
                <ol>
                    <li>Press the <kbd>Windows</kbd> and <kbd>R</kbd> buttons at the same time.</li>
                    <li>In the 'run' box that appears, enter this exact text: <pre>%appdata%\StardewValley\ErrorLogs</pre></li>
                    <li>The log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
                </ol>
            </div>
            <div id="@(Platform.Windows)-xbox">
                <ol>
                    <li>Press the <kbd>Windows</kbd> and <kbd>R</kbd> buttons at the same time.</li>
                    <li>In the 'run' box that appears, enter this exact text: <pre>%localappdata%\Packages\ConcernedApe.StardewValleyPC_0c8vynj4cqe4e\LocalCache\Roaming\StardewValley\ErrorLogs</pre></li>
                    <li>If you get an error with the title "Location is not available", try the "with Steam or GOG" instructions above.</li>
                    <li>Otherwise the log file is <code>SMAPI-crash.txt</code> if it exists, otherwise <code>SMAPI-latest.txt</code>.</li>
                </ol>
            </div>
        </div>
    </div>

    <h2>How do I share my log?</h2>
    <form action="@this.Url.PlainAction("Post", "LogParser")" method="post">
        <input id="inputFile" type="file" />
        <ol>
            <li>
                Drag the file onto this textbox <small>(or <a href="#" id="choose-file-link">choose a file</a>)</small>:<br />
                <textarea id="input" name="input" placeholder="paste log here"></textarea>
            </li>
            <li>
                Click this button:<br />
                <input type="submit" id="submit" value="save & parse log" />
            </li>
            <li>On the new page, copy the URL and send it to the person helping you.</li>
        </ol>
    </form>
}

@* parsed log *@
@if (log?.IsValid == true)
{
    <div id="output">
        @if (outdatedMods.Any() || missingErrorHandler || hasOlderErrorHandler || isPyTkCompatibilityMode || log.IsStrictMode)
        {
            <h2>Suggested fixes</h2>
            <ul id="fix-list">
                @if (missingErrorHandler)
                {
                    <li class="important">You don't have the <strong>Error Handler</strong> mod installed. This automatically prevents many game or mod errors. You can <a href="https://stardewvalleywiki.com/Modding:Player_Guide#Install_SMAPI">reinstall SMAPI</a> to re-add it.</li>
                }
                @if (hasOlderErrorHandler)
                {
                    <li>Your <strong>Error Handler</strong> mod is older than SMAPI. You may be missing some game/mod error fixes. You can <a href="https://stardewvalleywiki.com/Modding:Player_Guide#Install_SMAPI">reinstall SMAPI</a> to update it.</li>
                }
                @if (isPyTkCompatibilityMode)
                {
                    if (log.IsStrictMode)
                    {
                        <li>PyTK's image scaling isn't compatible with SMAPI strict mode.</li>
                    }
                    else
                    {
                        <li>PyTK 1.23.* or earlier isn't compatible with newer SMAPI performance optimizations. This may increase loading times or in-game lag.</li>
                    }
                }
                @if (outdatedMods.Any())
                {
                    <li>
                        Consider updating these mods to fix problems:

                        <table id="updates" class="table">
                            @foreach (LogModInfo mod in log.Mods.Where(mod => mod is { HasUpdate: true, IsContentPack: false } || (contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList) && contentPackList.Any(pack => pack.HasUpdate))))
                            {
                                <tr class="mod-entry">
                                    <td>
                                        <strong class=@(!mod.HasUpdate ? "hidden" : "")>@mod.Name</strong>
                                        @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out LogModInfo[]? contentPackList))
                                        {
                                            <div class="content-packs">
                                                @foreach (LogModInfo contentPack in contentPackList.Where(pack => pack.HasUpdate))
                                                {
                                                    <text>+ @contentPack.Name</text><br />
                                                }
                                            </div>
                                        }
                                    </td>
                                    <td>
                                        @if (mod.HasUpdate)
                                        {
                                            <a href="@mod.UpdateLink" target="_blank">
                                                @(mod.Version == null ? mod.UpdateVersion : $"{mod.Version} → {mod.UpdateVersion}")
                                            </a>
                                        }
                                        else
                                        {
                                            <text>&nbsp;</text>
                                        }

                                        @if (contentPacks != null && contentPacks.TryGetValue(mod.Name, out contentPackList))
                                        {
                                            <div>
                                                @foreach (LogModInfo contentPack in contentPackList.Where(pack => pack.HasUpdate))
                                                {
                                                    <a href="@contentPack.UpdateLink" target="_blank">@contentPack.Version → @contentPack.UpdateVersion</a><br />
                                                }
                                            </div>
                                        }
                                    </td>
                                </tr>
                            }
                        </table>
                    </li>
                }
                @if (log.IsStrictMode)
                {
                    <li class="notice">SMAPI is running in 'strict mode', which removes all deprecated APIs. This can significantly improve performance, but some mods may not work. You can <a href="https://stardewvalleywiki.com/Modding:Player_Guide#Install_SMAPI">reinstall SMAPI</a> to disable it if you run into problems.</li>
                }
            </ul>
        }

        <h2>Log info</h2>
        <table
            id="metadata"
            class="table"
            data-code-mods="@log.Mods.Count(p => p.IsCodeMod)"
            data-content-packs="@log.Mods.Count(p => p.IsContentPack)"
            data-os="@log.OperatingSystem"
            data-game-version="@log.GameVersion"
            data-game-path="@log.GamePath"
            data-smapi-version="@log.ApiVersion"
            data-log-started="@log.Timestamp.UtcDateTime.ToString("O")"
        >
            <caption>Game info:</caption>
            <tr>
                <th>Stardew Valley:</th>
                <td v-pre>@log.GameVersion on @log.OperatingSystem</td>
            </tr>
            <tr>
                <th>SMAPI:</th>
                <td v-pre>
                    @log.ApiVersion
                    @if (log.IsStrictMode)
                    {
                        <strong>(strict mode)</strong>
                    }
                </td>
            </tr>
            <tr>
                <th>Folder:</th>
                <td v-pre>@log.GamePath</td>
            </tr>
            <tr>
                <th>Log started:</th>
                <td>@log.Timestamp.UtcDateTime.ToString("yyyy-MM-dd HH:mm") UTC ({{localTimeStarted}} your time)</td>
            </tr>
        </table>
        <br />
        <table id="mods" class="@(Model.ShowRaw ? "filters-disabled" : null) table">
            <caption>
                Installed mods:
                @if (!Model.ShowRaw)
                {
                    <span class="notice txt"><i>click any mod to filter</i></span>
                    <span class="notice btn txt" v-on:click="showAllMods" v-bind:class="{ invisible: !anyModsHidden }">show all</span>
                    <span class="notice btn txt" v-on:click="hideAllMods" v-bind:class="{ invisible: !anyModsShown || !anyModsHidden }">hide all</span>
                    <span class="notice btn txt" v-on:click="toggleContentPacks">toggle content packs in list</span>
                }
            </caption>

            @{
                var modsWithContentPacks = log.Mods
                    .Where(mod => mod is { Loaded: true, IsContentPack: false })
                    .Select(mod => (
                        Mod: mod,
                        ContentPacks: contentPacks?.TryGetValue(mod.Name, out LogModInfo[]? contentPackList) == true ? contentPackList : []
                    ))
                    .ToList();
                if (contentPacks?.TryGetValue("", out LogModInfo[]? invalidPacks) == true)
                {
                    modsWithContentPacks.Add((
                        Mod: new LogModInfo(ModType.CodeMod, "<invalid content packs>", "", "", ""),
                        ContentPacks: invalidPacks
                    ));
                }
            }

            @foreach ((LogModInfo mod, LogModInfo[] contentPackList) in modsWithContentPacks)
            {
                <tr v-on:click="toggleMod('@Model.GetSlug(mod.Name)')" class="mod-entry" v-bind:class="{ hidden: !showMods['@Model.GetSlug(mod.Name)'] }">
                    <td><input type="checkbox" v-bind:checked="showMods['@Model.GetSlug(mod.Name)']" v-bind:class="{ invisible: !anyModsHidden }" /></td>
                    <td>
                        <strong v-pre>@mod.Name</strong> @mod.Version
                        @if (contentPackList.Any())
                        {
                            <div v-if="!hideContentPacks" class="content-packs">
                                @foreach (var contentPack in contentPackList)
                                {
                                    <text>+ @contentPack.Name @contentPack.Version</text><br />
                                }
                            </div>
                            <span v-else class="content-packs-collapsed"> (+ @contentPackList.Length content packs)</span>
                        }
                    </td>
                    <td>
                        @mod.Author
                        @if (contentPackList.Any())
                        {
                            <div v-if="!hideContentPacks" class="content-packs">
                                @foreach (var contentPack in contentPackList)
                                {
                                    <text>+ @contentPack.Author</text><br />
                                }
                            </div>
                        }
                    </td>
                    @if (mod.Errors == 0)
                    {
                        <td v-pre class="color-green">no errors</td>
                    }
                    else if (mod.Errors == 1)
                    {
                        <td v-pre class="color-red">@mod.Errors error</td>
                    }
                    else
                    {
                        <td v-pre class="color-red">@mod.Errors errors</td>
                    }
                </tr>
            }
        </table>

        @if (!Model.ShowRaw)
        {
            <div id="filterHolder"></div>
            <div id="filters">
                <div class="toggles">
                    <div>
                        Filter messages:
                    </div>
                    <div>
                        <span role="button" v-bind:class="{ active: showLevels['trace'] }" v-on:click="toggleLevel('trace')">TRACE</span> |
                        <span role="button" v-bind:class="{ active: showLevels['debug'] }" v-on:click="toggleLevel('debug')">DEBUG</span> |
                        <span role="button" v-bind:class="{ active: showLevels['info'] }" v-on:click="toggleLevel('info')">INFO</span> |
                        <span role="button" v-bind:class="{ active: showLevels['alert'] }" v-on:click="toggleLevel('alert')">ALERT</span> |
                        <span role="button" v-bind:class="{ active: showLevels['warn'] }" v-on:click="toggleLevel('warn')">WARN</span> |
                        <span role="button" v-bind:class="{ active: showLevels['error'] }" v-on:click="toggleLevel('error')">ERROR</span>
                        <div class="filter-text">
                            <input
                                type="text"
                                v-bind:class="{ active: !!filterText }"
                                v-model="filterText"
                                v-on:input="updateFilterText"
                                placeholder="search to filter log..."
                            />
                            <span role="button" v-bind:class="{ active: filterUseRegex }" v-on:click="toggleFilterUseRegex" title="Use regular expression syntax.">.*</span>
                            <span role="button" v-bind:class="{ active: !filterInsensitive }" v-on:click="toggleFilterInsensitive" title="Match exact capitalization only.">aA</span>
                            <span role="button" v-bind:class="{ active: filterUseWord, 'whole-word': true }" v-on:click="toggleFilterWord" title="Match whole word only."><i>“ ”</i></span>
                            <span role="button" v-bind:class="{ active: shouldHighlight }" v-on:click="toggleHighlight" title="Highlight matches in the log text.">HL</span>
                            <div
                                v-if="filterError"
                                class="filter-error"
                            >
                                {{ filterError }}
                            </div>
                        </div>
                        <filter-stats
                            v-bind:start="start"
                            v-bind:end="end"
                            v-bind:pages="totalPages"
                            v-bind:filtered="filteredMessages.total"
                            v-bind:total="totalMessages"
                        />
                    </div>
                </div>
                <pager
                    v-bind:page="page"
                    v-bind:pages="totalPages"
                    v-bind:prevPage="prevPage"
                    v-bind:nextPage="nextPage"
                />
            </div>

            <noscript>
                <div>
                    This website uses JavaScript to display a filterable table. To view this log, please enable JavaScript or <a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteId, format = LogViewFormat.RawView })">view the raw log</a>.
                </div>
                <br />
            </noscript>

            <log-table>
                <log-line
                    v-for="msg in visibleMessages"
                    v-bind:key="msg.id"
                    v-bind:message="msg"
                    v-bind:highlight="shouldHighlight"
                />
            </log-table>
        }
        else
        {
            <pre v-pre>@log.RawText</pre>
        }

        <small>
            @if (Model.ShowRaw)
            {
                <a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteId })">view parsed log</a>
            }
            else
            {
                <a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteId, format = LogViewFormat.RawView })">view raw log</a>
            }

            | <a href="@this.Url.PlainAction("Index", "LogParser", new { id = Model.PasteId, format = LogViewFormat.RawDownload })" download>download</a>
        </small>
    </div>
}
else if (log?.IsValid == false)
{
    <h3>Raw log</h3>
    <pre v-pre>@log.RawText</pre>
}
